Flutter TextEditingController – Detailed Notes
TextEditingController is a Flutter class used to control, read, modify, clear, and listen to the text entered inside a TextField or TextFormField. It is especially useful when an application needs to access user input programmatically.
Flutter's official documentation describes TextEditingController as a controller for an editable text field. It can provide an initial value, expose the current text and selection, notify listeners when the value changes, and update the text field programmatically.
1. What is TextEditingController?
A TextEditingController acts as a bridge between your Dart code and a text input widget.
It allows you to:
- Read text entered by the user.
- Set an initial value.
- Change the text programmatically.
- Clear the text field.
- Listen for text changes.
- Access the current selection.
- Use text input in login, registration, search, profile, and other forms.
Basic Syntax
final TextEditingController controller = TextEditingController();
2. Import Required Package
For Material-based Flutter applications, import:
import 'package:flutter/material.dart';
3. Creating a TextEditingController
Create a controller as a field inside the State class when it belongs to a stateful form or screen.
class MyScreenState extends State {
final TextEditingController nameController = TextEditingController();
@override
void dispose() {
nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
The controller should generally be disposed when it is no longer needed. This releases resources associated with the controller.
4. Connecting TextEditingController to TextField
Use the controller property of TextField.
final TextEditingController nameController = TextEditingController();
TextField(
controller: nameController,
)
After connecting the controller, the controller can be used to retrieve or modify the text.
5. Reading Text from TextEditingController
The text property returns the current text.
String name = nameController.text;
print(name);
Complete Example
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const UserInputScreen(),
);
}
}
class UserInputScreen extends StatefulWidget {
const UserInputScreen({super.key});
@override
State createState() => _UserInputScreenState();
}
class _UserInputScreenState extends State {
final TextEditingController nameController = TextEditingController();
@override
void dispose() {
nameController.dispose();
super.dispose();
}
void showName() {
print(nameController.text);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TextEditingController'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Enter your name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: showName,
child: const Text('Read Name'),
),
],
),
),
);
}
}
6. Getting Text When Button is Pressed
A common use case is collecting information after the user presses a button.
final TextEditingController emailController = TextEditingController();
ElevatedButton(
onPressed: () {
String email = emailController.text;
print('Email: $email');
},
child: const Text('Submit'),
)
7. Setting Initial Text
You can create a controller with initial text by using the text parameter.
final TextEditingController nameController =
TextEditingController(text: 'Manish');
When this controller is connected to a TextField, the field initially displays Manish.
Example
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State {
final TextEditingController nameController =
TextEditingController(text: 'John');
@override
void dispose() {
nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
),
),
),
);
}
}
8. Changing Text Programmatically
You can change the current text using the text property.
nameController.text = 'Flutter Developer';
For example:
ElevatedButton(
onPressed: () {
nameController.text = 'Flutter Developer';
},
child: const Text('Set Text'),
)
Changing the controller's text updates the connected text field.
9. Clearing Text
The clear() method removes the current text.
nameController.clear();
Example
ElevatedButton(
onPressed: () {
nameController.clear();
},
child: const Text('Clear'),
)
This is commonly used after submitting a message, search query, or todo item.
10. Checking Whether Text is Empty
You can check whether the user has entered anything.
if (nameController.text.isEmpty) {
print('Name is required');
} else {
print('Name: ${nameController.text}');
}
Using trim()
trim() removes unnecessary whitespace from the beginning and end of a string.
final String name = nameController.text.trim();
if (name.isEmpty) {
print('Please enter your name');
}
11. Listening to Text Changes
A TextEditingController can notify your code whenever its value changes. Use addListener() to register a listener.
final TextEditingController searchController =
TextEditingController();
@override
void initState() {
super.initState();
searchController.addListener(() {
print(searchController.text);
});
}
The listener runs whenever the controller's value changes.
Complete Listener Example
class SearchScreen extends StatefulWidget {
const SearchScreen({super.key});
@override
State createState() => _SearchScreenState();
}
class _SearchScreenState extends State {
final TextEditingController searchController =
TextEditingController();
@override
void initState() {
super.initState();
searchController.addListener(_handleSearch);
}
void _handleSearch() {
print('Search: ${searchController.text}');
}
@override
void dispose() {
searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: searchController,
decoration: const InputDecoration(
hintText: 'Search',
border: OutlineInputBorder(),
),
),
),
);
}
}
12. TextEditingController vs onChanged
Both onChanged and TextEditingController can be used to detect changes in text.
| Feature |
onChanged |
TextEditingController |
| Read current text |
Yes |
Yes |
| Set text programmatically |
No |
Yes |
| Clear field programmatically |
No |
Yes |
| Listen to changes |
Yes |
Yes |
| Access selection |
No |
Yes |
| Initial value |
Not directly |
Yes |
| Requires dispose() |
No |
Yes when owned by your State/widget lifecycle |
onChanged Example
TextField(
onChanged: (value) {
print(value);
},
)
Controller Example
final controller = TextEditingController();
TextField(
controller: controller,
)
print(controller.text);
13. Using TextEditingController with TextFormField
TextEditingController can also be connected to TextFormField.
final TextEditingController emailController =
TextEditingController();
TextFormField(
controller: emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
)
This is especially useful when working with forms and validation.
14. TextEditingController with Form Validation
A controller can be used to retrieve the validated input after the form has been successfully validated.
final _formKey = GlobalKey();
final emailController = TextEditingController();
Form(
key: _formKey,
child: TextFormField(
controller: emailController,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Email is required';
}
return null;
},
),
)
Submitting the Form
if (_formKey.currentState!.validate()) {
print(emailController.text);
}
15. Multiple TextEditingControllers
In a registration or profile form, you can create separate controllers for each input field.
final nameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final passwordController = TextEditingController();
Using the Controllers
Column(
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Name',
),
),
TextField(
controller: emailController,
decoration: const InputDecoration(
labelText: 'Email',
),
),
TextField(
controller: phoneController,
decoration: const InputDecoration(
labelText: 'Phone',
),
),
TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
),
),
],
)
16. Disposing Multiple Controllers
Every controller owned by the screen should be disposed when the screen is removed.
@override
void dispose() {
nameController.dispose();
emailController.dispose();
phoneController.dispose();
passwordController.dispose();
super.dispose();
}
This is an important part of managing controller resources correctly.
17. Complete Registration Form Example
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const RegistrationScreen(),
);
}
}
class RegistrationScreen extends StatefulWidget {
const RegistrationScreen({super.key});
@override
State createState() => _RegistrationScreenState();
}
class _RegistrationScreenState extends State {
final nameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final passwordController = TextEditingController();
@override
void dispose() {
nameController.dispose();
emailController.dispose();
phoneController.dispose();
passwordController.dispose();
super.dispose();
}
void registerUser() {
final name = nameController.text.trim();
final email = emailController.text.trim();
final phone = phoneController.text.trim();
final password = passwordController.text;
print('Name: $name');
print('Email: $email');
print('Phone: $phone');
print('Password: $password');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Registration'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Full Name',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Phone',
prefixIcon: Icon(Icons.phone),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: registerUser,
child: const Text('Register'),
),
),
],
),
),
);
}
}
18. Login Form Example
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
final emailController = TextEditingController();
final passwordController = TextEditingController();
@override
void dispose() {
emailController.dispose();
passwordController.dispose();
super.dispose();
}
void login() {
final email = emailController.text.trim();
final password = passwordController.text;
if (email.isEmpty || password.isEmpty) {
print('Please enter email and password');
return;
}
print('Login Email: $email');
print('Login Password: $password');
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: login,
child: const Text('Login'),
),
],
),
),
);
}
}
19. Building a Search Field
One practical use of TextEditingController is creating a search interface.
final searchController = TextEditingController();
TextField(
controller: searchController,
decoration: InputDecoration(
hintText: 'Search products',
prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
searchController.clear();
},
),
border: const OutlineInputBorder(),
),
)
Reading Search Text
void searchProducts() {
final query = searchController.text.trim();
if (query.isEmpty) {
return;
}
print('Searching for: $query');
}
20. Updating Text from a Button
A controller is useful when the application needs to change input automatically.
ElevatedButton(
onPressed: () {
nameController.text = 'Flutter Student';
},
child: const Text('Use Default Name'),
)
21. Resetting a Form
You can clear multiple controllers to reset a form.
void resetForm() {
nameController.clear();
emailController.clear();
phoneController.clear();
passwordController.clear();
}
Reset Button
OutlinedButton(
onPressed: resetForm,
child: const Text('Reset'),
)
22. TextEditingController and Text Selection
The controller also provides access to the current text selection through the selection property.
print(nameController.selection);
You can use selection information when building advanced text-editing features.
Select All Text
nameController.selection = TextSelection(
baseOffset: 0,
extentOffset: nameController.text.length,
);
23. Using TextEditingValue
TextEditingController extends Flutter's value-notifier system and exposes the complete editing state through the value property.
print(nameController.value);
The value can contain information such as:
- Current text.
- Selection.
- Composing range.
Example
nameController.value = TextEditingValue(
text: 'Flutter',
selection: const TextSelection.collapsed(offset: 7),
);
Using value can be useful when you need to update text together with selection information.
24. Important Warning About Listeners
If you modify the controller inside its own listener, the listener may be triggered again. This can create an unintended loop.
Problematic Pattern
controller.addListener(() {
controller.text = controller.text.toUpperCase();
});
This pattern can repeatedly modify the controller while the listener is responding to its own changes.
Better Approach
For simple transformations, consider using input formatters or carefully guard the listener so that it only changes the value when necessary.
25. TextEditingController with Input Formatting
For restrictions such as numbers, length limits, or specific character patterns, Flutter provides input formatters.
TextField(
controller: phoneController,
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
)
When using this example, import:
import 'package:flutter/services.dart';
26. Practical Example: Character Counter
A controller can be used with a listener to update a character count.
class MessageScreen extends StatefulWidget {
const MessageScreen({super.key});
@override
State createState() => _MessageScreenState();
}
class _MessageScreenState extends State {
final messageController = TextEditingController();
@override
void initState() {
super.initState();
messageController.addListener(_updateCount);
}
void _updateCount() {
setState(() {});
}
@override
void dispose() {
messageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: messageController,
maxLines: 5,
decoration: const InputDecoration(
labelText: 'Message',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
Text(
'Characters: ${messageController.text.length}',
),
],
),
),
);
}
}
27. Practical Example: Todo Input
class TodoScreen extends StatefulWidget {
const TodoScreen({super.key});
@override
State createState() => _TodoScreenState();
}
class _TodoScreenState extends State {
final controller = TextEditingController();
final List todos = [];
@override
void dispose() {
controller.dispose();
super.dispose();
}
void addTodo() {
final text = controller.text.trim();
if (text.isEmpty) {
return;
}
setState(() {
todos.add(text);
});
controller.clear();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Todo App'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: controller,
decoration: const InputDecoration(
hintText: 'Enter a task',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: addTodo,
child: const Text('Add'),
),
],
),
),
Expanded(
child: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
leading: const Icon(Icons.check_circle_outline),
title: Text(todos[index]),
);
},
),
),
],
),
);
}
}
28. TextEditingController Lifecycle
A typical controller lifecycle contains three important stages:
- Create: Create the controller as a state field.
- Use: Connect it to a
TextField or TextFormField.
- Dispose: Call
dispose() when the owning widget is removed.
class ExampleState extends State {
final controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
);
}
}
29. Common Mistakes
Mistake 1: Forgetting dispose()
final controller = TextEditingController();
// Missing dispose()
Better:
@override
void dispose() {
controller.dispose();
super.dispose();
}
Mistake 2: Creating the Controller Inside build()
Avoid creating a new controller every time build() runs.
@override
Widget build(BuildContext context) {
final controller = TextEditingController();
return TextField(controller: controller);
}
This can cause the controller to be recreated during rebuilds.
Better: Keep the controller as a field of the appropriate State object.
Mistake 3: Reading the Wrong Controller
When multiple fields are present, use descriptive controller names.
final emailController = TextEditingController();
final passwordController = TextEditingController();
Mistake 4: Not Trimming User Input
final name = nameController.text.trim();
Using trim() can prevent accidental leading or trailing spaces from being treated as meaningful input.
30. Best Practices
- Use a separate controller for each field when you need programmatic access to that field.
- Dispose controllers that your widget owns.
- Use
trim() when appropriate before processing user-entered text.
- Use
TextFormField with a Form when structured validation is required.
- Use
onChanged for simple change handling when a controller is not otherwise needed.
- Use
addListener() when you need centralized observation of controller changes.
- Avoid unnecessary controller creation inside
build().
- Use input formatters for input restrictions rather than manually modifying text whenever possible.
- Be careful when changing controller values from inside listeners.
- Use meaningful controller names such as
emailController and passwordController.
31. TextEditingController Important Properties and Methods
| Property/Method |
Purpose |
text |
Gets or sets the current text. |
value |
Gets or sets the complete editing value. |
selection |
Gets or sets the current text selection. |
clear() |
Removes the current text. |
addListener() |
Registers a callback for controller changes. |
removeListener() |
Removes a previously registered listener. |
dispose() |
Releases resources when the controller is no longer needed. |
hasListeners |
Indicates whether listeners are registered. |
32. TextField and TextEditingController Relationship
TextEditingController
|
| controller:
v
TextField / TextFormField
|
v
User enters text
|
v
Controller value changes
|
v
Dart code reads or modifies the value
The controller provides programmatic control over the editable text field.
33. Real-World Applications
TextEditingController is commonly useful in:
- Login forms.
- Registration forms.
- Search bars.
- Profile editing screens.
- Chat applications.
- Comment sections.
- Todo applications.
- Contact forms.
- Checkout forms.
- Address forms.
- Product search.
- Data submission screens.
- API request forms.
34. Interview Questions
Q1. What is TextEditingController?
Answer: It is a Flutter controller used to control and access the text and editing state of a TextField or TextFormField.
Q2. How do you get text from a controller?
String text = controller.text;
Q3. How do you clear a TextField?
controller.clear();
Q4. How do you set text programmatically?
controller.text = 'Hello Flutter';
Q5. Why should a controller be disposed?
Answer: Controllers are disposable objects. Calling dispose() when the owning widget is finished with the controller releases resources and removes registered listeners associated with it.
Q6. Can TextEditingController be used with TextFormField?
Answer: Yes. A controller can be supplied to both TextField and TextFormField.
Q7. What is the difference between onChanged and TextEditingController?
Answer: onChanged is a callback for receiving changed text, while TextEditingController provides broader programmatic control, including reading, changing, clearing, listening to, and inspecting the editing value.
35. Quick Revision
TextEditingController controls editable text.
- Connect it using the
controller property.
- Read input with
controller.text.
- Set input with
controller.text = '...'.
- Clear input with
controller.clear().
- Listen for changes with
controller.addListener().
- Use
controller.value for complete editing information.
- Use
controller.selection for selection management.
- Dispose the controller when the owning widget is disposed.
- Use controllers extensively in forms, search interfaces, login screens, registration screens, and data-entry applications.
36. Official Flutter Resources
37. JustAcademy Flutter Training Resources
Learn more about Flutter development through the following resources:
38. Key Takeaways
TextEditingController is one of the most important tools for working with user-entered text in Flutter. It provides direct access to the text field's current value and allows developers to read, update, clear, monitor, and manage text programmatically. It works with both TextField and TextFormField and is particularly useful for forms, authentication interfaces, search functionality, API data submission, and other interactive Flutter applications.